/*
  xnrg_26_mk_sky_blu.ino - MakeSkyBlue Solar charger support for Tasmota

  Copyright (C) 2025  meMorizE

  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#ifdef USE_ENERGY_SENSOR
#ifdef USE_MAKE_SKY_BLUE
/*********************************************************************************************\
 * This implementation communicates with solar charge controller of MakeSkyBlue(Shenzen) Co.LTD 
 * http://www.makeskyblue.com
 * Model: S3-30A/40A/50A/60A MPPT 12V/24V/36V/48V with firmwware V118 (or V119 including a Wifi Box)
 * Tested with Model S3-60A
 * https://makeskyblue.com/en-de/products/60a-mppt-solar-charge-controller-w-wifi
 *
 * Precondition to get communication working:
 * The charge controller should have a Mini-USB socket at bottom right beside the screw terminals.
 * It has a Vcc supply output of +5V and GND but the D+ and D- signals are NOT according USB standard !
 * They are TTL serial signals with 9600bps 8N1, D+=Tx_Out D-=Rx_In
 * When using a DIY ESP hardware be aware of the 5V TTL levels by using e.g. level shifter or isolator
 * 
 * This implementation supports multiple charge controllers at one tasmota-ESP (by 2025-11):
 * ESP82xx up to 3
 * ESP32xx up to 8 (theoretically, not tested if so many serial interfaces will work)
 * Every serial receiver channel is related to one energy phase.
 * It allows to have only one transmitter and multiple receivers, but this does
 * not allow individual addressing for on/off and register read/write.
 * 
 * The orignal V119 Wifi Box hardware includes an ESP8285 with 1MB Flash (as module 2AL3B ESP-M)
 * This specific hardware uses
 *  GPIO1: TX0,  ESP => Charge controller
 *  GPIO3: RXD0, ESP <= Charge controller
 *  GPIO5 red LED, active low, for e.g. LedLink_i
 *  free (or bootstrap): GPIO4, GPIO12, GPIO13, GPIO14, GPIO15, GPIO16, GPIO17(ADC)
 *  {"NAME":"MakeSkyBlue Wifi-Adapter (ESP8285)","GPIO":[1,10528,1,10560,1,1,0,0,1,1,1,1,1,1],"FLAG":0,"BASE":18}
 * 
 * Useful runtime commands and options:
 *  VoltRes 1              - select voltage resolution to 0.1 V (native resolution of solar charger)
 *  AmpRes 1               - select current resolution to 0.1 A (native resolution of solar charger)
 *  SetOption72            - read and use total energy from the memory within the charge controller
 *  SetOption129 1         - Display energy for each phase instead of single sum (only if multiple channels configured)
 *  SetOption150 1         - Display no common voltage/frequency
 * 
 * This implementation is based on information from
 * https://github.com/lasitha-sparrow/Makeskyblue_wifi_Controller
 * https://www.genetrysolar.com/community/forum-134/topic-1219/
 * 
 * and logic analyzer recording of the original Wifi-Box firmware together with the Android app:
 * Status:       - request periodical every 1s
 *               - response typical within 30ms
 * Measurements: - request periodical every 1s, about 380ms after Status request 
 *               - response varies within 30...370ms
 * ReadRegister: - requested on demand with an interval of min. 170ms, valid registers 1...9
 *               - response typical within 30ms
\*********************************************************************************************/

#include <TasmotaSerial.h>

#define XNRG_26                     26


/* available compile options, bit encoded */
#if MAKE_SKY_BLUE_OPTION & 0x1
#define MKSB_WITH_SERIAL_DEBUGGING  // provide counters for error detection and statistics
// >Practical use: a real device shows frequent timeouts and / or CRC errors, typically if the solar power is more than 350W.
#endif
//
#if MAKE_SKY_BLUE_OPTION & 0x2
#define MKSB_WITH_ON_OFF_SUPPORT    // allow to switch charging OFF and ON
// >Use Console command 'EnergyConfig 0 -' to switch OFF
// >Use Console command 'EnergyConfig 0 +' to switch ON
#endif
//
#if MAKE_SKY_BLUE_OPTION & 0x4
#define MKSB_WITH_REGISTER_SUPPORT  // read and write access to configuration registers
// >Use Console command 'EnergyConfig' to read and write configuration registers R1...R9
//  no parameters: show help
//  1st parameter:  i = transmit interface, 0 for all
//  2nd parameter:  n = address value of specific register number n
//  3rd parameters: v = value to write to specified register number n, none=read
#endif


#define MKSB_BAUDRATE               9600
// TxRx first byte of every valid frame
#define MKSB_START_FRAME            0xAA
// Tx second byte: Request to the charge controller
#define MKSB_CMD_READ_MEASUREMENTS  0x55 // response: 0xBB
#define MKSB_CMD_READ_REGISTER      0xCB // response: 0xDA
#define MKSB_CMD_WRITE_REGISTER     0xCA // response: 0xDA
#define MKSB_CMD_AUXILARY           0xCC // response: 0xDC or 0xDD
// Rx second byte: Response from the charge controller
#define MKSB_RSP_READ_MEASURES      0xBB // request: 0xAA
#define MKSB_RSP_RW_CONFIG          0xDA // request: 0xCB or 0xCA
#define MKSB_RSP_CLR_WIFI_PASSWORD  0xDC // D05 Clear Wifi-Password (not implemented)
#define MKSB_RSP_POWER              0xDD // request 0xCC
//
#define MKSB_RSP_SZ_READ_MEASURES  20 // bytes // minimum size of response frame
#define MKSB_RSP_SZ_RW_CONFIG       9 // bytes // size of response frame


// TxRx third byte: config registers (request 0xCB or 0xCA, response 0xDA)
#define MKSB_REG_FIRST              1    // vvv = related local parameter at display UI
#define MKSB_REG_VOLTAGE_BULK       1    // D02 MPPT Voltage limit BULK, >= stops charging [mV], e.g. 55000mV
#define MKSB_REG_VOLTAGE_FLOAT      2    // D01 MPPT voltage limit FLOAT, <= restarts charging [mV], SLA battery only
#define MKSB_REG_OUT_TIMER          3    // D00 Time duration the load gets connected [mh], default: 24h = 24000mh
#define MKSB_REG_CURRENT            4    //  -  MPPT current limit [mA] e.g. 1000...60000mA, CAUTION: respect limit of hardware
#define MKSB_REG_BATT_UVP_CUT       5    // D03 Battery UnderVoltageProtection, <=limit cuts load [mV], e.g. 42400mV
#define MKSB_REG_BATT_UVP_CONN      6    //  ~  Battery UnderVoltageProtection, >=limit reconnects load [mV], typical: D03 + 200mV, e.g. 44400mV
#define MKSB_REG_COMM_ADDR          7    //  -  Communication Address [mAddress]
#define MKSB_REG_BATT_TYPE          8    // D04 Battery Type: value 0=SLA, 1=LiPo (2=LiLo, 3=LiFE, 4=LiTo) [mSelect], e.g. 1000=LiPo
#define MKSB_REG_BATT_CELLS         9    //  -  Battery System: 1...4 * 12V (Read-Only, set at Batt.connection) [m12V], e.g. 4000m12V
#define MKSB_REG_LAST               9    // ^^^ = related local parameter at display UI
#define MKSB_REG_TOTAL              (1+(MKSB_REG_LAST-MKSB_REG_FIRST))

#define MKSB_RX_BUFFER_SIZE         24 // bytes, 20 minimum
#define MKSB_TX_BUFFER_SIZE         8  // bytes,  7 minimum

#define MKSB_STATUS_MPPT_IDLE           0       // Idle, HMI=3.0 Night Mode (PV < XYZ V)
#define MKSB_STATUS_MPPT_OCP_OUTPUT     2       // Overcurrent Protection, ??? E73
#define MKSB_STATUS_MPPT_OVP_OUTPUT     3       // MPPT Bulk Voltage Limit reached
#define MKSB_STATUS_MPPT_CHARGING       4       // Charging, HMI=4.0 MPPT Mode
#define MKSB_STATUS_MPPT_FULL           6       // Battery full
//
#define MKSB_STATUS_BATT_UVP            0x100   // Battery Undervoltage Protection, load cut, Fault E65
#define MKSB_STATUS_BATT_OVP            0x200   // Battery Overvoltage, load still connected, Fault E63

// module type definition
typedef struct MKSB_MODULE_T_
{
    uint8_t idx;                          // index of receiver interface
    uint8_t phase_id;                     // phase id
    uint8_t idx_tx;                       // index of transmitter interface
    TasmotaSerial *Serial = nullptr;
    char *pRxBuffer = nullptr;
    uint8_t txBuffer[MKSB_TX_BUFFER_SIZE];
    uint16_t energy_total;                // totalizer at charge controller (non-volatile there)
//    uint16_t status;                      // combines mode (LSB) and status / error (MSB)
    uint8_t rxIdx;                        // bytecounter at the Rx data buffer
    uint8_t rxChecksum;                   // for checksum calculation at reception
    uint8_t ev250ms_state;                // for scheduled serial requesting, snyced with everysecond
#ifdef MKSB_WITH_SERIAL_DEBUGGING
    uint32_t cntTx;                       // count requests transmitted to the charge controller
    uint32_t cntRxGood;                   // count valid responses received from the charge controller
    uint32_t cntRxBadCRC;                 // count CRC-invalid responses
    uint32_t tsTx;                        // timestamp in ms of last byte transmitted
    uint32_t tsRx;                        // timestamp in ms of last byte received
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
    uint16_t regs_to_read;                // bit_n = flags register n to read
    uint16_t regs_to_write;               // bit_n = flags register n to write
    uint16_t regs_to_report;              // bit_n = flags register n to report (after valid response received)
    uint16_t regs_value[MKSB_REG_TOTAL];  // value storage of all known configuration registers
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
    bool actual_state;                    // true = active, false = stop
    bool target_state;                    // true = active, false = stop
#endif
} MKSB_MODULE_T;


MKSB_MODULE_T *pMskbInstance = nullptr;
float *pMksbInstance_FloatArrays = nullptr; // single allocation reference for all channel float arrays
enum _E_MKSB_FLOATS
{
    MKSB_INSTANCE_FLOATARRAY_TEMPERATURE = 0,
    MKSB_INSTANCE_FLOATARRAY_BATTVOLTAGE,
    MKSB_INSTANCE_FLOATARRAY_BATTCURRENT,
    MKSB_INSTANCE_FLOATARRAY_SIZE           // number of different float arrays
} E_MKSB_FLOATS;
// Note: each float array has Energy->phase_count elements

// module const data
const char mksb_HTTP_SNS_TEMPERATURE[]    PROGMEM = "{s}" D_TEMPERATURE             "{m}%s °%c{e}";
const char mksb_HTTP_SNS_BATT_VOLTAGE[]   PROGMEM = "{s}" D_BATTERY " " D_VOLTAGE   "{m}%s " D_UNIT_VOLT          "{e}";
const char mksb_HTTP_SNS_BATT_CURRENT[]   PROGMEM = "{s}" D_BATTERY " " D_CURRENT   "{m}%s " D_UNIT_AMPERE        "{e}";
//const char mksb_HTTP_SNS_STATUS_INFO[]    PROGMEM = "{s}" D_STATUS                  "{m}%s {e}";
//
#ifdef MKSB_WITH_REGISTER_SUPPORT
// config register format strings: requires one float as register value
static const char mksb_fsrreg1[] PROGMEM = "NRG: CH%u R1 = %1_fV [D02] MPPT Bulk Charging Voltage";
static const char mksb_fsrreg2[] PROGMEM = "NRG: CH%u R2 = %1_fV [D01] MPPT Floating Charging Voltage (SLA only)";
static const char mksb_fsrreg3[] PROGMEM = "NRG: CH%u R3 = %0_fh [D00] Load-Output ON duration";
static const char mksb_fsrreg4[] PROGMEM = "NRG: CH%u R4 = %1_fA MPPT Current Limit (CAUTION: change on your own risk!)";
static const char mksb_fsrreg5[] PROGMEM = "NRG: CH%u R5 = %1_fV [D03] Battery Undervoltage Protection (Load-Output OFF)";
static const char mksb_fsrreg6[] PROGMEM = "NRG: CH%u R6 = %1_fV Battery Undervoltage Recovery (Load-Output ON)";
static const char mksb_fsrreg7[] PROGMEM = "NRG: CH%u R7 = %0_f Com Address (not used)";
static const char mksb_fsrreg8[] PROGMEM = "NRG: CH%u R8 = %0_f [D04] Battery Type [0=SLA, 1=Li]";
static const char mksb_fsrreg9[] PROGMEM = "NRG: CH%u R9 = %0_f Battery System [1...4 * 12V] (read-only)";
static const char * mksb_register_fstrings[] PROGMEM = { mksb_fsrreg1, mksb_fsrreg2, mksb_fsrreg3, 
                                                         mksb_fsrreg4, mksb_fsrreg5, mksb_fsrreg6, 
                                                         mksb_fsrreg7, mksb_fsrreg8, mksb_fsrreg9 }; 
#endif


/********************************************************************************************/
/* EXTRACT UNSIGNED INT FROM SERIAL RECEIVED DATA */ 
uint32_t mExtractUint32(const char *data, uint8_t offset_lsb, uint8_t offset_msb)
{
    uint32_t result = 0;

  for ( ; offset_msb >= offset_lsb; offset_msb-- ) {
    result = (result << 8) | (uint8_t)data[offset_msb];
  }
    return result;
}


/********************************************************************************************/
/* FINALIZE SERIAL RECEIVE */ 
static void mFinishReceive(void * me)
{
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;

    #ifdef MKSB_WITH_SERIAL_DEBUGGING
    if ( mksb->rxIdx ) { // bytes received with timediff since last request
        if ( mksb->idx_tx ) { // channel is tranceiver, show time since last send
            mksb->tsRx = millis();
            AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Tx+ %ums] %*_H"),
                mksb->idx, (mksb->tsRx - mksb->tsTx), mksb->rxIdx, mksb->pRxBuffer);
        } else { // channel is receiver only, show time since last receive-finish
            mksb->tsTx = millis(); // use it for now
            AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: Rx%u [Rx+ %ums] %*_H"),
                mksb->idx, (mksb->tsTx - mksb->tsRx), mksb->rxIdx, mksb->pRxBuffer);
            mksb->tsRx = mksb->tsTx;
        }
    }
    #endif
    // finalize reception
    mksb->Serial->flush(); // ensure receive buffer is empty
    mksb->rxIdx = 0; // reset receiver state
}


/********************************************************************************************/
/* SEND SERIAL DATA */
static void mSendSerial(void * me, uint8_t len)
{
    uint32_t i;
    uint8_t crc;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
  
    if (mksb->idx_tx == 0) {
        return; // the channel has no transmitter, receive only
    }

    mksb->Serial->flush(); // ensure receive buffer is empty
    mksb->rxIdx = 0; // reset receiver state

    // request frame
    mksb->Serial->write( MKSB_START_FRAME );
    crc = 0;
    for ( i = 0; i < len; i++ ) { // variable data
        mksb->Serial->write(mksb->txBuffer[i]);
        crc += mksb->txBuffer[i];
    }
    mksb->Serial->write(crc); // checksum
    mksb->Serial->flush(); // ensure transmission complete

#ifdef MKSB_WITH_SERIAL_DEBUGGING
    mksb->tsTx = millis();
    mksb->cntTx++;
    AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("NRG: CH%u Tx: %02X%*_H%02X"), 
        mksb->idx, MKSB_START_FRAME, len, mksb->txBuffer, crc );
#endif
}


/********************************************************************************************/
/* PARSE SERIAL DATA RECEIVED */
static void mParseMeasurements(void * me)
{
    uint8_t phase;
    uint32_t voltage, current, power;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;

    phase = mksb->phase_id;
    // solar
    voltage = mExtractUint32(mksb->pRxBuffer,  6, 7); // voltage   [0.1V]
    power   = mExtractUint32(mksb->pRxBuffer,  8, 9); // power     [1.0W]
    Energy->voltage[phase] = (float)voltage / 10.0f;
    Energy->active_power[phase] = (float)power;
    if ( voltage ) { // prevent division by 0
        // calculate: solar current I = P / U
        Energy->current[phase] = Energy->active_power[phase] / Energy->voltage[phase]; 
    } else {
        Energy->current[phase] = 0.0f;
    }
    Energy->data_valid[phase] = 0;

    // battery
    current = mExtractUint32(mksb->pRxBuffer,  4, 5); // charge current [0.1A]
    voltage = mExtractUint32(mksb->pRxBuffer,  2, 3); // voltage        [0.1V]

    // temperature of the charge controller electronics, integer resolution is 0.1degC
    pMksbInstance_FloatArrays[phase] = ConvertTempToFahrenheit( (float)mExtractUint32(mksb->pRxBuffer, 10, 11) / 10.0f );
    phase += Energy->phase_count;
    // battery voltage, integer resolution is 0.1V
    pMksbInstance_FloatArrays[phase] = (float)voltage / 10.0f;
    phase += Energy->phase_count;
    // battery charge current, integer resolution is 0.1A
    pMksbInstance_FloatArrays[phase] = (float)current / 10.0f;
    mksb->energy_total = mExtractUint32(mksb->pRxBuffer, 12, 13); // solar energy total [1.0kWh]
 
    // mksb->status = mExtractUint32(mksb->pRxBuffer, 16, 17); // mode and status

    // unused response data: unknown encoding
    // mExtractUint32(mksb->pRxBuffer, 14, 15); // ?dummy [14:15]
    // mExtractUint32(mksb->pRxBuffer, 18, 18); // ?dummy [18]
}


/********************************************************************************************/
/* RECEIVE SERIAL DATA */
static void MkSkyBluSerialReceive(void * me)
{
    int i;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;

    if (mksb->Serial == nullptr) {
        return; // serial interface not available
    }

    while ( mksb->Serial->available() ) {
        yield();
        if (mksb->rxIdx < MKSB_RX_BUFFER_SIZE) { // buffer available
            mksb->pRxBuffer[mksb->rxIdx] = mksb->Serial->read();
            if (MKSB_START_FRAME != mksb->pRxBuffer[0] ) { // no start of frame yet
                mksb->rxIdx = 0; // reset receiver
            } else {                // [0] start valid
                if (0 == mksb->rxIdx) {  // start of frame present
                    mksb->rxChecksum = 0;  // reset checksum calc
                } else {              // [1+] cmd or later
                    if ((MKSB_RSP_READ_MEASURES == mksb->pRxBuffer[1]) && (19 == mksb->rxIdx)) {
                        if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
                            mParseMeasurements(mksb);
#ifdef MKSB_WITH_SERIAL_DEBUGGING
                            mksb->cntRxGood++;
                        } else {
                            mksb->cntRxBadCRC++;
#endif
                        }
                        mFinishReceive(me);
                    } else
#ifdef MKSB_WITH_REGISTER_SUPPORT
                    if ((MKSB_RSP_RW_CONFIG == mksb->pRxBuffer[1]) && (8 == mksb->rxIdx)) 
                    {
                        uint8_t reg;
                        if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
                            reg = mksb->pRxBuffer[2] - MKSB_REG_FIRST;
                            if ( reg < MKSB_REG_TOTAL ) {
                                mksb->regs_value[reg] = mExtractUint32(mksb->pRxBuffer, 3, 4);
                                mksb->regs_to_report |= 1 << reg;
                            }
#ifdef MKSB_WITH_SERIAL_DEBUGGING
                            mksb->cntRxGood++;                    
                        } else {
                            mksb->cntRxBadCRC++;
#endif
                        }
                        mFinishReceive(me);
                    } else
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
                    if ((MKSB_RSP_POWER == mksb->pRxBuffer[1]) && (5 == mksb->rxIdx)) {
                        if ( mksb->rxChecksum == mksb->pRxBuffer[mksb->rxIdx] ) {
                            if ( mksb->pRxBuffer[2] == 0 ) { /* ON */
                                if ( mksb->actual_state != true ) {
                                    AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging ON"), mksb->idx);
                                    mksb->actual_state = true;
                                }
                            } else 
                            if ( mksb->pRxBuffer[2] == 1 ) {  /* OFF */
                                if ( mksb->actual_state != false ) {
                                    AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u Charging OFF"), mksb->idx);
                                    mksb->actual_state = false;
                                }
                            } else { /* unknown content */
                            }
#ifdef MKSB_WITH_SERIAL_DEBUGGING
                            mksb->cntRxGood++;
                        } else {
                            mksb->cntRxBadCRC++;
#endif
                        }
                        mFinishReceive(me);
                    } else 
#endif
                    { // calc checksum
                        mksb->rxChecksum += mksb->pRxBuffer[mksb->rxIdx]; 
                    }
                } 
            }
            mksb->rxIdx++;       // more to receive         
        } else { // buffer full
            mFinishReceive(me);
        }
    } 
}


/********************************************************************************************/
/* EVERY SECOND */
void MkSkyBluEverySecond(void * me)
{
    int i;
    float fValue;
    uint8_t phase;
    bool updateTotal = false;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;

    phase = mksb->phase_id;

    if (Energy->data_valid[phase] > ENERGY_WATCHDOG) {
        Energy->voltage[phase] = Energy->current[phase] = Energy->active_power[phase] = 0.0f;
        Energy->voltage[phase] = NAN; // mark as invalid
        Energy->current[phase] = NAN; // mark as invalid
        pMksbInstance_FloatArrays[phase] = NAN; // temperature
        phase += Energy->phase_count;
        pMksbInstance_FloatArrays[phase] = NAN; // battery voltage
        phase += Energy->phase_count;
        pMksbInstance_FloatArrays[phase] = NAN; // battery current
    } else {
        Energy->kWhtoday_delta[phase] += Energy->active_power[phase] * 1000 / 36; // solar energy only
        // import the non-resetable solar energy full kWh counter from the charge controller, but with 2 requirements:
        // 1. SetOption72 is active (bug@EnergyUpdateTotal?: a call of it impacts kWhtoday, even if option is off)
        // 2. the tasmota total is smaller than the total of the charge controller
        if ( Settings->flag3.hardware_energy_total ) {
            fValue = (float)mksb->energy_total;
            if ( Energy->total[phase] < fValue ) {
                Energy->import_active[phase] = fValue;
                updateTotal = true;
            }
        }
    }
    mksb->ev250ms_state = 0; // sync the 250ms state machine

#ifdef MKSB_WITH_SERIAL_DEBUGGING
    {
        static uint32_t lastDebugLog = 0;
        if ( (millis() - lastDebugLog) > (5u * 60000u) ) { // every 5 minutes
            if ( mksb->idx_tx ) { // transceiver
                AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Tx:%u, Rx+:%u, Rx-total:%u (Rx-CRC:%u)"), 
                    mksb->idx, mksb->cntTx, mksb->cntRxGood, mksb->cntTx - mksb->cntRxGood, mksb->cntRxBadCRC );
            } else { // receiver only
                AddLog(LOG_LEVEL_INFO, PSTR("NRG: CH%u serial statistics Rx+:%u, Rx-CRC:%u"),
                    mksb->idx, mksb->cntRxGood, mksb->cntRxBadCRC );
            }
            if (mksb->phase_id >= (Energy->phase_count - 1) ) { // last channel logged
                lastDebugLog = millis();
            }
        }
    }
#endif
    if ( updateTotal == true ) {
        EnergyUpdateTotal(); // this also calls EnergyUpdateToday() at the end
    } else {
        EnergyUpdateToday();
    }
}


/********************************************************************************************/
/* EVERY 250 MS */
void MkSkyBluEvery250ms(void * me)
{ 
    static const uint8_t mksb_ser_req_measures[]  PROGMEM = { MKSB_CMD_READ_MEASUREMENTS, 0,0,0 };
    static const uint8_t mksb_ser_req_write_reg[] PROGMEM = { MKSB_CMD_WRITE_REGISTER, 0 ,0,0, 0,0,0 };
    static const uint8_t mksb_ser_req_read_reg[]  PROGMEM = { MKSB_CMD_READ_REGISTER,  0 ,0,0, 0,0,0 };
    static const uint8_t mksb_ser_req_chrg_off[]  PROGMEM = { MKSB_CMD_AUXILARY, 1, 2, 0 };
    static const uint8_t mksb_ser_req_chrg_on[]   PROGMEM = { MKSB_CMD_AUXILARY, 2, 0, 0 };
    int i;
    float fVal;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;

    if (mksb->Serial == nullptr) {
        return; // serial interface not available
    }
    if (mksb->idx_tx == 0) {
        return; // no transmitter at this channel
    }

    // if ( mksb->ev250ms_state == 0 ) {
    //     MkSkyBluRequestStatus();
    //  } else
    if ( mksb->ev250ms_state == 0 ) {
        memcpy_P(mksb->txBuffer, mksb_ser_req_measures, sizeof (mksb_ser_req_measures));
        mSendSerial(mksb, sizeof (mksb_ser_req_measures));
    } 
#ifdef MKSB_WITH_REGISTER_SUPPORT
    else if ( mksb->ev250ms_state == 3 ) {
        if ( mksb->regs_to_read || mksb->regs_to_write ) {
            for ( i = 0; i < MKSB_REG_TOTAL; i++ ) {
                if ( mksb->regs_to_write & (1 << i) ) { // prio write
                    memcpy_P(mksb->txBuffer, mksb_ser_req_write_reg, sizeof (mksb_ser_req_write_reg));
                    // variable register and value
                    mksb->txBuffer[1] = i + MKSB_REG_FIRST;
                    mksb->txBuffer[2] = (uint8_t)(mksb->regs_value[i] & 0xFF);
                    mksb->txBuffer[3] = (uint8_t)(mksb->regs_value[i] >> 8);
                    mSendSerial(mksb, sizeof (mksb_ser_req_write_reg));
                    mksb->regs_to_write &= ~(1 << i);
                    break; // only one per call
                } else 
                if ( mksb->regs_to_read & (1 << i) ) { 
                    memcpy_P(mksb->txBuffer, mksb_ser_req_read_reg, sizeof (mksb_ser_req_read_reg));
                    // variable register
                    mksb->txBuffer[1] = i + MKSB_REG_FIRST;
                    mSendSerial(mksb, sizeof (mksb_ser_req_read_reg));
                    mksb->regs_to_read &= ~(1 << i);
                    break; // only one per call
                } else {}
            }
        }
    }
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
    else if ( mksb->actual_state != mksb->target_state ) {
        if ( mksb->target_state == false ) { 
            memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_off, sizeof (mksb_ser_req_chrg_off));
            mSendSerial(mksb, sizeof (mksb_ser_req_chrg_off));
        } else { 
            memcpy_P(mksb->txBuffer, mksb_ser_req_chrg_on, sizeof (mksb_ser_req_chrg_on));
            mSendSerial(mksb, sizeof (mksb_ser_req_chrg_on));
        }
        mksb->actual_state = mksb->target_state;
    }
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
    if ( mksb->regs_to_report ) 
    {
        for ( i = 0; i < MKSB_REG_TOTAL; i++ ) {
            if ( mksb->regs_to_report & (1 << i) ) {
                fVal = ((float)(mksb->regs_value[i])) / 1000.0f;
                AddLog( LOG_LEVEL_ERROR, mksb_register_fstrings[i], mksb->idx, &fVal); // log always
                mksb->regs_to_report &= ~(1 << i);
                break; // once per call
            }
        }
    }
#endif
    mksb->ev250ms_state++;
    if ( mksb->ev250ms_state >= 4 ) 
    {
        mksb->ev250ms_state = 0;
    }
}


/********************************************************************************************/
/* ENERGY COMMAND */
bool MkSkyBluEnergyCommand(void * me)
{
    bool serviced = true;
    uint8_t reg;
    char *str;
    int32_t value;
    int i;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me; 

    if ((CMND_POWERCAL == Energy->command_code) || (CMND_VOLTAGECAL == Energy->command_code) || (CMND_CURRENTCAL == Energy->command_code)) {
        // Service in xdrv_03_energy.ino
    } else 
    if (CMND_ENERGYCONFIG == Energy->command_code) {
        AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: EnergyConfig index %d, payload %d, data '%s'"),
        XdrvMailbox.index, XdrvMailbox.payload, XdrvMailbox.data ? XdrvMailbox.data : "null" );
        if ( XdrvMailbox.data_len == 0 ) { // no arguments: show help
            AddLog(LOG_LEVEL_INFO, PSTR("NRG: Usage: EnergyConfig <Tx-Interface> [+,-,Register-Num:1...9] [Register-Value]"));
        } else {
            str = XdrvMailbox.data;
            // 1st argument: transmit interface number 1...8, 0=all
            i = strtoul( str, &str, 10 ); 
            if( i != mksb->idx_tx && i != 0 ) {
                return serviced; // this instance: not addressed or no transmitter
            }
            while ((*str != '\0') && isspace(*str)) { str++; };   // Trim spaces
            // 2nd argument: +,- or register number
#ifdef MKSB_WITH_ON_OFF_SUPPORT
            if ('-' == str[0] ) {             // to set controller charging off
                mksb->target_state = false;
                return serviced;
            }
            if ('+' == str[0] ) {             // to set controller charging active
                mksb->target_state = true;
                return serviced;
            }
#endif
#ifdef MKSB_WITH_REGISTER_SUPPORT
            reg = (uint8_t)strtoul( str, &str, 10 ) - MKSB_REG_FIRST;
            if ( MKSB_REG_FIRST <= reg && MKSB_REG_LAST >= reg ) 
            {   // valid register number 1...9
                while ((*str != '\0') && isspace(*str)) { str++; }   // Trim spaces
                if ( *str ) 
                {   // 3nd argument: there is a value = write registers value
                    value = (int32_t)(CharToFloat(str) * 1000.0f);
                    // write Register: no range check here !
                    mksb->regs_value[reg] = value;   // store value to prepared for write
                    mksb->regs_to_write |= 1 << reg; // trigger write
                } else 
                {   // 3rd argument: there is no value = read registers value
                    mksb->regs_to_read |= 1 << reg; 
                }
                return serviced;
            } else 
            {   // invalid register number
                mksb->regs_to_read = (1 << MKSB_REG_TOTAL) - 1; // flag all registers to be read        
                return serviced;
            }
#endif
            serviced = false;
        }
    } else {
        serviced = false;  // Unknown command
    }
    return serviced;
}


/********************************************************************************************/
/* PUBLISH SENSORS (beyond Energy) */
static void MkSkyBluShow(uint32_t function) 
{
    uint8_t phase;
    bool voltage_common = (Settings->flag6.no_voltage_common) ? false : Energy->voltage_common;

    if ( FUNC_JSON_APPEND == function ) { 
        phase = 0;
        // Efficiency: not used for JSON
        phase += Energy->phase_count;
        // Temperature
        ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%s"),
            EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution));
        phase += Energy->phase_count;
        // Battery Voltage
        ResponseAppend_P(PSTR(",\"" D_JSON_VOLTAGE " battery\":%s"),
            EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution, voltage_common));
        phase += Energy->phase_count;
        // Battery Current
        ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT " battery\":%s"),
            EnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution));
    }

#ifdef USE_WEBSERVER
    if ( FUNC_WEB_COL_SENSOR == function ) {
        phase = 0;
        WSContentSend_PD(mksb_HTTP_SNS_TEMPERATURE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.temperature_resolution), TempUnit());
        phase += Energy->phase_count;
        WSContentSend_PD(mksb_HTTP_SNS_BATT_VOLTAGE, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.voltage_resolution));
        phase += Energy->phase_count;
        WSContentSend_PD(mksb_HTTP_SNS_BATT_CURRENT, WebEnergyFmt(&pMksbInstance_FloatArrays[phase], Settings->flag2.current_resolution));
    }
    else if ( FUNC_WEB_SENSOR == function ) 
    {
        WSContentSend_P( PSTR("MakeSkyBlue " D_SOLAR_POWER " " D_CHARGE) ); // headline after values
    } else {}
#endif  // USE_WEBSERVER
}


/********************************************************************************************/
/* Reset ENERGY */
void MkSkyBluReset(void * me)
{
    int i;
    uint8_t phase;
    MKSB_MODULE_T * mksb = (MKSB_MODULE_T *)me;
    
    // mksb->temperature = NAN;
    mksb->energy_total = 0;

    phase = mksb->phase_id;
    Energy->total[phase] = 0.0f;
//    Energy->data_valid[phase] = 0;
}


/********************************************************************************************/
/* SENSORS INIT */
void MkSkyBluSnsInit(void * me)
{
    int i;
    MKSB_MODULE_T * mksb;

    mksb = (MKSB_MODULE_T *)me;
    i = mksb->idx - 1; // inteface receiver index
    // Software serial init needs to be done here as earlier (serial) interrupts may lead to Exceptions
    if ( mksb->idx_tx ) { // transceiver
        mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), Pin(GPIO_MKSKYBLU_TX, i), 1);
    } else { // receiver only
        mksb->Serial = new TasmotaSerial(Pin(GPIO_MKSKYBLU_RX, i), -1, 1);
    }
    if (mksb->Serial == nullptr) {
        AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial alloc failed"), mksb->idx);
        return;
    }
    if (mksb->Serial->begin(MKSB_BAUDRATE)) {
        //    mksb->pTxBuffer = (char*)(malloc(MKSB_TX_BUFFER_SIZE));
        if (mksb->Serial->hardwareSerial()) {
            ClaimSerial();
            // Use idle serial buffer to save RAM
            mksb->pRxBuffer = TasmotaGlobal.serial_in_buffer + 
                sizeof(TasmotaGlobal.serial_in_buffer) - mksb->idx * MKSB_RX_BUFFER_SIZE;  
            // from the end, this allows to use other sensors from start simultaneously
        } else {
            mksb->pRxBuffer = (char*)(malloc(MKSB_RX_BUFFER_SIZE));
        }
#ifdef ESP32
        AddLog(LOG_LEVEL_DEBUG, PSTR("NRG: CH%d ESP32 Serial UART%d"), mksb->idx, mksb->Serial->getUart());
#endif
    } else {
        mksb->Serial = nullptr;
        AddLog(LOG_LEVEL_ERROR, PSTR("NRG: CH%d Serial init failed"), mksb->idx);
    }
}


/********************************************************************************************/
/* DRIVER INIT */
void MkSkyBluDrvInit(void)
{
    int i;
    MKSB_MODULE_T * mksb = nullptr;
    uint8_t phase = 0, u8 = 0;

    for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) {
        if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver
            phase++; // count configured receiver
            if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // check for configured transmitter
                u8++; // count configured transceiver
            }
        }
    }
    if ( !u8 ) { // at least one transceiver needed
        return; // mkskyblu not configured, driver not active
    } else
    if (Energy == nullptr ) { // something is wrong with the tasmota energy support
        return;
    } else
    if( phase > sizeof( Energy->data_valid ) ) { // limit to max supported phases, 2025-11-02: 3 at ESP82xx, 8 at ESP32
        phase = sizeof( Energy->data_valid );
        AddLog(LOG_LEVEL_INFO, PSTR("NRG: Channel count limited to %d"), phase);
    }
    // one instance per configured receiver
    pMskbInstance = (MKSB_MODULE_T *)(malloc(phase * sizeof(MKSB_MODULE_T)));
    pMksbInstance_FloatArrays = (float *)(malloc(phase * (MKSB_INSTANCE_FLOATARRAY_SIZE * sizeof(float))));
    if ( pMskbInstance == nullptr || pMksbInstance_FloatArrays == nullptr ) {
        AddLog(LOG_LEVEL_ERROR, PSTR("NRG: Memory allocation failed"));
        return;
    }
    // at this point we have at least one transceiver configured
    mksb = pMskbInstance; // first instance
    phase = 0;
    for( i = 0; i < MAX_MKSKYBLU_IF; i++ ) {
        if ( PinUsed(GPIO_MKSKYBLU_RX, i) ) { // check for configured receiver
            mksb->idx = i + 1; // interface receiver index
            if ( PinUsed(GPIO_MKSKYBLU_TX, i) ) { // transceiver
                mksb->idx_tx = mksb->idx; // interface transmitter index
            } else {
                mksb->idx_tx = 0; // unknown related transmitter
            }
            mksb->Serial = nullptr;
            mksb->pRxBuffer = nullptr; // allocated at SnsInit
            mksb->phase_id = phase++;
            // preset / fixed module values
            mksb->energy_total = 0;
#ifdef MKSB_WITH_REGISTER_SUPPORT
            mksb->regs_to_read = 0;
            mksb->regs_to_write = 0;
            mksb->regs_to_report = 0;
#endif
#ifdef MKSB_WITH_ON_OFF_SUPPORT
            mksb->actual_state = true; // default: charging enabled
            mksb->target_state = true; // default: charging enabled
#endif
            mksb++; // next instance
        }
    }
    for( i = 0; i < (phase * MKSB_INSTANCE_FLOATARRAY_SIZE); i++ ) {
        pMksbInstance_FloatArrays[i] = NAN;
    }
    // preset / fixed energy values
    Energy->phase_count = phase;
    Energy->voltage_common = false;   // every charge controller has an individual solar voltage
    Energy->frequency_common = true;
    Energy->type_dc = true;           // solar dc charger
    Energy->use_overtemp = false;     // ESP device acts as separated gateway, charge controller has its own temperature management
    Energy->voltage_available = true; // solar power and voltage is provided by serial communication
    Energy->current_available = true; // solar current is calculated from power and voltage
    AddLog(LOG_LEVEL_INFO, PSTR("NRG: MakeSkyBlue driver initialized with %d channel(s)"), Energy->phase_count);
    TasmotaGlobal.energy_driver = XNRG_26;
}


/*********************************************************************************************\
 * Interface
\*********************************************************************************************/

bool Xnrg26(uint32_t function)
{
    bool result = false;
    int i;
    MKSB_MODULE_T * mksb;

    if ( function == FUNC_PRE_INIT ) {
        MkSkyBluDrvInit(); // create all instances
        return result;
    } else

    for( i = 0; i < Energy->phase_count; i++ ) 
    {
        mksb = &pMskbInstance[i];
        if ( (void *)mksb == nullptr ) {
            continue; // instance not configured
        }
        switch (function) {
        case FUNC_LOOP:
            MkSkyBluSerialReceive((void *)mksb);
            break;
        case FUNC_EVERY_250_MSECOND:
            MkSkyBluEvery250ms((void *)mksb);
            break;
        case FUNC_ENERGY_EVERY_SECOND:
            MkSkyBluEverySecond((void *)mksb);
            break;
        case FUNC_JSON_APPEND:
        case FUNC_WEB_SENSOR:
        case FUNC_WEB_COL_SENSOR:
            MkSkyBluShow(function);
            return result; // one call for all instances
            break;
        case FUNC_ENERGY_RESET:
//            MkSkyBluReset((void *)mksb); // not used
            break;
        case FUNC_COMMAND:
            result = MkSkyBluEnergyCommand((void *)mksb);
            break;
        case FUNC_INIT:
            MkSkyBluSnsInit((void *)mksb);
            break;
        }
    }
    return result;
}

#endif  // USE_MAKE_SKY_BLUE
#endif  // USE_ENERGY_SENSOR
